 /*******************************************************************************
  * Copyright (c) 2002, 2006 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM - Initial API and implementation
  *******************************************************************************/
 package org.eclipse.core.internal.resources.refresh.win32;

 import java.io.File ;
 import java.util.*;
 import org.eclipse.core.internal.refresh.RefreshManager;
 import org.eclipse.core.internal.utils.Messages;
 import org.eclipse.core.resources.IResource;
 import org.eclipse.core.resources.ResourcesPlugin;
 import org.eclipse.core.resources.refresh.IRefreshMonitor;
 import org.eclipse.core.resources.refresh.IRefreshResult;
 import org.eclipse.core.runtime.*;
 import org.eclipse.core.runtime.jobs.Job;
 import org.eclipse.osgi.util.NLS;
 import org.osgi.framework.Bundle;

 /**
  * A monitor that works on Win32 platforms. Provides simple notification of
  * entire trees by reporting that the root of the tree has changed to depth
  * DEPTH_INFINITE.
  */
 class Win32Monitor extends Job implements IRefreshMonitor {
     private static final long RESCHEDULE_DELAY = 1000;

     /**
      * A ChainedHandle is a linked list of handles.
      */
     protected abstract class ChainedHandle extends Handle {
         private ChainedHandle next;
         private ChainedHandle previous;

         public abstract boolean exists();

         public ChainedHandle getNext() {
             return next;
         }

         public ChainedHandle getPrevious() {
             return previous;
         }

         public void setNext(ChainedHandle next) {
             this.next = next;
         }

         public void setPrevious(ChainedHandle previous) {
             this.previous = previous;
         }
     }

     protected class FileHandle extends ChainedHandle {
         private File file;

         public FileHandle(File file) {
             this.file = file;
         }

         public boolean exists() {
             return file.exists();
         }

         public void handleNotification() {
             if (!isOpen())
                 return;
             ChainedHandle next = getNext();
             if (next != null) {
                 if (next.isOpen()) {
                     if (!next.exists()) {
                         if (next instanceof LinkedResourceHandle) {
                             next.close();
                             LinkedResourceHandle linkedResourceHandle = (LinkedResourceHandle) next;
                             linkedResourceHandle.postRefreshRequest();
                         } else {
                             next.close();
                         }
                         ChainedHandle previous = getPrevious();
                         if (previous != null)
                             previous.open();
                     }
                 } else {
                     next.open();
                     if (next.isOpen()) {
                         Handle previous = getPrevious();
                         previous.close();
                         if (next instanceof LinkedResourceHandle)
                             ((LinkedResourceHandle) next).postRefreshRequest();
                     }
                 }
             }
             findNextChange();
         }

         public void open() {
             if (!isOpen()) {
                 Handle next = getNext();
                 if (next != null && next.isOpen()) {
                     openHandleOn(file);
                 } else {
                     if (exists()) {
                         openHandleOn(file);
                     }
                     Handle previous = getPrevious();
                     if (previous != null) {
                         previous.open();
                     }
                 }
             }
         }
     }

     protected abstract class Handle {
         protected long handleValue;

         public Handle() {
             handleValue = Win32Natives.INVALID_HANDLE_VALUE;
         }

         public void close() {
             if (isOpen()) {
                 if (!Win32Natives.FindCloseChangeNotification(handleValue)) {
                     int error = Win32Natives.GetLastError();
                     if (error != Win32Natives.ERROR_INVALID_HANDLE)
                         addException(NLS.bind(Messages.WM_errCloseHandle, Integer.toString(error)));
                 }
                 if (RefreshManager.DEBUG)
                     System.out.println(DEBUG_PREFIX + "removed handle: " + handleValue); //$NON-NLS-1$
 handleValue = Win32Natives.INVALID_HANDLE_VALUE;
             }
         }

         private long createHandleValue(String path, boolean monitorSubtree, int flags) {
             long handle = Win32Natives.FindFirstChangeNotification(path, monitorSubtree, flags);
             if (handle == Win32Natives.INVALID_HANDLE_VALUE) {
                 int error = Win32Natives.GetLastError();
                 addException(NLS.bind(Messages.WM_errCreateHandle, path, Integer.toString(error)));
             }
             return handle;
         }

         public void destroy() {
             close();
         }

         protected void findNextChange() {
             if (!Win32Natives.FindNextChangeNotification(handleValue)) {
                 int error = Win32Natives.GetLastError();
                 if (error != Win32Natives.ERROR_INVALID_HANDLE && error != Win32Natives.ERROR_SUCCESS) {
                     addException(NLS.bind(Messages.WM_errFindChange, Integer.toString(error)));
                 }
                 removeHandle(this);
             }
         }

         public long getHandleValue() {
             return handleValue;
         }

         public abstract void handleNotification();

         public boolean isOpen() {
             return handleValue != Win32Natives.INVALID_HANDLE_VALUE;
         }

         public abstract void open();

         protected void openHandleOn(File file) {
             openHandleOn(file.getAbsolutePath(), false);
         }

         protected void openHandleOn(IResource resource) {
             openHandleOn(resource.getLocation().toOSString(), true);
         }

         private void openHandleOn(String path, boolean subtree) {
             setHandleValue(createHandleValue(path, subtree, Win32Natives.FILE_NOTIFY_CHANGE_FILE_NAME | Win32Natives.FILE_NOTIFY_CHANGE_DIR_NAME | Win32Natives.FILE_NOTIFY_CHANGE_LAST_WRITE | Win32Natives.FILE_NOTIFY_CHANGE_SIZE));
             if (isOpen()) {
                 fHandleValueToHandle.put(new Long (getHandleValue()), this);
                 setHandleValueArrays(createHandleArrays());
             } else {
                 close();
             }
         }

         protected void postRefreshRequest(IResource resource) {
             //native callback occurs even if resource was changed within workspace
 if (!resource.isSynchronized(IResource.DEPTH_INFINITE))
                 refreshResult.refresh(resource);
         }

         public void setHandleValue(long handleValue) {
             this.handleValue = handleValue;
         }
     }

     protected class LinkedResourceHandle extends ChainedHandle {
         private List fileHandleChain;
         private IResource resource;

         /**
          * @param resource
          */
         public LinkedResourceHandle(IResource resource) {
             this.resource = resource;
             createFileHandleChain();
         }

         protected void createFileHandleChain() {
             fileHandleChain = new ArrayList(1);
             File file = new File (resource.getLocation().toOSString());
             file = file.getParentFile();
             while (file != null) {
                 fileHandleChain.add(0, new FileHandle(file));
                 file = file.getParentFile();
             }
             int size = fileHandleChain.size();
             for (int i = 0; i < size; i++) {
                 ChainedHandle handle = (ChainedHandle) fileHandleChain.get(i);
                 handle.setPrevious((i > 0) ? (ChainedHandle) fileHandleChain.get(i - 1) : null);
                 handle.setNext((i + 1 < size) ? (ChainedHandle) fileHandleChain.get(i + 1) : this);
             }
             setPrevious((size > 0) ? (ChainedHandle) fileHandleChain.get(size - 1) : null);
         }

         public void destroy() {
             super.destroy();
             for (Iterator i = fileHandleChain.iterator(); i.hasNext();) {
                 Handle handle = (Handle) i.next();
                 handle.destroy();
             }
         }

         public boolean exists() {
             IPath location = resource.getLocation();
             return location == null ? false : location.toFile().exists();
         }

         public void handleNotification() {
             if (isOpen()) {
                 postRefreshRequest(resource);
                 findNextChange();
             }
         }

         public void open() {
             if (!isOpen()) {
                 if (exists()) {
                     openHandleOn(resource);
                 }
                 FileHandle handle = (FileHandle) getPrevious();
                 if (handle != null && !handle.isOpen()) {
                     handle.open();
                 }
             }
         }

         public void postRefreshRequest() {
             postRefreshRequest(resource);
         }
     }

     protected class ResourceHandle extends Handle {
         private IResource resource;

         public ResourceHandle(IResource resource) {
             super();
             this.resource = resource;
         }

         public IResource getResource() {
             return resource;
         }

         public void handleNotification() {
             if (isOpen()) {
                 postRefreshRequest(resource);
                 findNextChange();
             }
         }

         public void open() {
             if (!isOpen()) {
                 openHandleOn(resource);
             }
         }
     }

     private static final String DEBUG_PREFIX = "Win32RefreshMonitor: "; //$NON-NLS-1$
 private static final int WAIT_FOR_MULTIPLE_OBJECTS_TIMEOUT = 300;
     /**
      * Any errors that have occurred
      */
     protected MultiStatus errors;
     /**
      * Arrays of handles, split evenly when the number of handles is larger
      * than Win32Natives.MAXIMUM_WAIT_OBJECTS
      */
     protected long[][] fHandleValueArrays;
     /**
      * Mapping of handles (java.lang.Long) to absolute paths
      * (java.lang.String).
      */
     protected Map fHandleValueToHandle;
     protected IRefreshResult refreshResult;

     /*
      * Creates a new monitor. @param result A result that will recieve refresh
      * callbacks and error notifications
      */
     public Win32Monitor(IRefreshResult result) {
         super(Messages.WM_jobName);
         this.refreshResult = result;
         setPriority(Job.DECORATE);
         setSystem(true);
         fHandleValueToHandle = new HashMap(1);
         setHandleValueArrays(createHandleArrays());
     }

     /**
      * Logs an exception
      */
     protected synchronized void addException(String message) {
         if (errors == null) {
             String msg = Messages.WM_errors;
             errors = new MultiStatus(ResourcesPlugin.PI_RESOURCES, 1, msg, null);
         }
         errors.add(new Status(IStatus.ERROR, ResourcesPlugin.PI_RESOURCES, 1, message, null));
     }

     /*
      * Splits the given array into arrays of length no greater than <code> max
      * </code> . The lengths of the sub arrays differ in size by no more than
      * one element. <p> Examples: <ul><li> If an array of size 11 is split
      * with a max of 4, the resulting arrays are of size 4, 4, and 3. </li>
      * <li> If an array of size 18 is split with a max of 5, the resulting
      * arrays are of size 5, 5, 4, and 4. </li></ul>
      */
     private long[][] balancedSplit(final long[] array, final int max) {
         int elementCount = array.length;
         // want to handle [1, max] rather than [0, max)
 int subArrayCount = ((elementCount - 1) / max) + 1;
         int subArrayBaseLength = elementCount / subArrayCount;
         int overflow = elementCount % subArrayCount;
         long[][] result = new long[subArrayCount][];
         int count = 0;
         for (int i = 0; i < subArrayCount; i++) {
             int subArrayLength = subArrayBaseLength + (overflow-- > 0 ? 1 : 0);
             long[] subArray = new long[subArrayLength];
             for (int j = 0; j < subArrayLength; j++) {
                 subArray[j] = array[count++];
             }
             result[i] = subArray;
         }
         return result;
     }

     private Handle createHandle(IResource resource) {
         if (resource.isLinked())
             return new LinkedResourceHandle(resource);
         return new ResourceHandle(resource);
     }

     /*
      * Since the Win32Natives.WaitForMultipleObjects(...) method cannot accept
      * more than a certain number of objects, we are forced to split the array
      * of objects to monitor and monitor each one individually. <p> This method
      * splits the list of handles into arrays no larger than
      * Win32Natives.MAXIMUM_WAIT_OBJECTS. The arrays are balenced so that they
      * differ in size by no more than one element.
      */
     protected long[][] createHandleArrays() {
         long[] handles;
         // synchronized: in order to protect the map during iteration
 synchronized (fHandleValueToHandle) {
             Set keys = fHandleValueToHandle.keySet();
             int size = keys.size();
             if (size == 0) {
                 return new long[0][0];
             }
             handles = new long[size];
             int count = 0;
             for (Iterator i = keys.iterator(); i.hasNext();) {
                 handles[count++] = ((Long ) i.next()).longValue();
             }
         }
         return balancedSplit(handles, Win32Natives.MAXIMUM_WAIT_OBJECTS);
     }

     private Handle getHandle(IResource resource) {
         if (resource == null) {
             return null;
         }
         // synchronized: in order to protect the map during iteration
 synchronized (fHandleValueToHandle) {
             for (Iterator i = fHandleValueToHandle.values().iterator(); i.hasNext();) {
                 Handle handle = (Handle) i.next();
                 if (handle instanceof ResourceHandle) {
                     ResourceHandle resourceHandle = (ResourceHandle) handle;
                     if (resourceHandle.getResource().equals(resource)) {
                         return handle;
                     }
                 }
             }
         }
         return null;
     }

     /*
      * Answers arrays of handles. The handles are split evenly when the number
      * of handles becomes larger than Win32Natives.MAXIMUM_WAIT_OBJECTS.
      * @return long[][]
      */
     private long[][] getHandleValueArrays() {
         return fHandleValueArrays;
     }

     /**
      * Adds a resource to be monitored by this native monitor
      */
     public boolean monitor(IResource resource) {
         IPath location = resource.getLocation();
         if (location == null) {
             // cannot monitor remotely managed containers
 return false;
         }
         Handle handle = createHandle(resource);
         // synchronized: handle creation must be atomic
 synchronized (this) {
             handle.open();
         }
         if (!handle.isOpen()) {
             //ignore errors if we can't even create a handle on the resource
 //it will fall back to polling anyway
 errors = null;
             return false;
         }
         //make sure the job is running
 schedule(RESCHEDULE_DELAY);
         if (RefreshManager.DEBUG)
             System.out.println(DEBUG_PREFIX + " added monitor for: " + resource); //$NON-NLS-1$
 return true;
     }

     /**
      * Removes the handle from the <code>fHandleValueToHandle</code> map.
      *
      * @param handle
      * a handle, not <code>null</code>
      */
     protected void removeHandle(Handle handle) {
         List handles = new ArrayList(1);
         handles.add(handle);
         removeHandles(handles);
     }

     /**
      * Removes all of the handles in the given collection from the <code>fHandleValueToHandle</code>
      * map. If collections from the <code>fHandleValueToHandle</code> map are
      * used, copy them before passing them in as this method modifies the
      * <code>fHandleValueToHandle</code> map.
      *
      * @param handles
      * a collection of handles, not <code>null</code>
      */
     private void removeHandles(Collection handles) {
         // synchronized: protect the array, removal must be atomic
 synchronized (this) {
             for (Iterator i = handles.iterator(); i.hasNext();) {
                 Handle handle = (Handle) i.next();
                 fHandleValueToHandle.remove(new Long (handle.getHandleValue()));
                 handle.destroy();
             }
             setHandleValueArrays(createHandleArrays());
         }
     }

     /*
      * @see java.lang.Runnable#run()
      */
     protected IStatus run(IProgressMonitor monitor) {
         long start = -System.currentTimeMillis();
         if (RefreshManager.DEBUG)
             System.out.println(DEBUG_PREFIX + "job started."); //$NON-NLS-1$
 try {
             long[][] handleArrays = getHandleValueArrays();
             monitor.beginTask(Messages.WM_beginTask, handleArrays.length);
             // If changes occur to the list of handles,
 // ignore them until the next time through the loop.
 for (int i = 0, length = handleArrays.length; i < length; i++) {
                 if (monitor.isCanceled())
                     return Status.CANCEL_STATUS;
                 waitForNotification(handleArrays[i]);
                 monitor.worked(1);
             }
         } finally {
             monitor.done();
             start += System.currentTimeMillis();
             if (RefreshManager.DEBUG)
                 System.out.println(DEBUG_PREFIX + "job finished in: " + start + "ms"); //$NON-NLS-1$ //$NON-NLS-2$
 }
         //always reschedule the job - so it will come back after errors or cancelation
 //make sure it doesn't hog more that 5% of CPU
 long delay = Math.max(RESCHEDULE_DELAY, start * 30);
         if (RefreshManager.DEBUG)
             System.out.println(DEBUG_PREFIX + "rescheduling in: " + delay / 1000 + " seconds"); //$NON-NLS-1$ //$NON-NLS-2$
 final Bundle bundle = Platform.getBundle(ResourcesPlugin.PI_RESOURCES);
         //if the bundle is null then the framework has shutdown - just bail out completely (bug 98219)
 if (bundle == null)
             return Status.OK_STATUS;
         //don't reschedule the job if the resources plugin has been shut down
 if (bundle.getState() == Bundle.ACTIVE)
             schedule(delay);
         MultiStatus result = errors;
         errors = null;
         //just log native refresh failures
 if (result != null && !result.isOK())
             ResourcesPlugin.getPlugin().getLog().log(result);
         return Status.OK_STATUS;
     }

     protected void setHandleValueArrays(long[][] arrays) {
         fHandleValueArrays = arrays;
     }

     /* (non-Javadoc)
      * @see org.eclipse.core.runtime.jobs.Job#shouldRun()
      */
     public boolean shouldRun() {
         return !fHandleValueToHandle.isEmpty();
     }

     /*
      * @see org.eclipse.core.resources.refresh.IRefreshMonitor#unmonitor(IContainer)
      */
     public void unmonitor(IResource resource) {
         if (resource == null) {
             // resource == null means stop monitoring all resources
 synchronized (fHandleValueToHandle) {
                 removeHandles(new ArrayList(fHandleValueToHandle.values()));
             }
         } else {
             Handle handle = getHandle(resource);
             if (handle != null)
                 removeHandle(handle);
         }
         //stop the job if there are no more handles
 if (fHandleValueToHandle.isEmpty())
             cancel();
     }

     /**
      * Performs the native call to wait for notification on one of the given
      * handles.
      *
      * @param handleValues
      * an array of handles, it must contain no duplicates.
      */
     private void waitForNotification(long[] handleValues) {
         int handleCount = handleValues.length;
         int index = Win32Natives.WaitForMultipleObjects(handleCount, handleValues, false, WAIT_FOR_MULTIPLE_OBJECTS_TIMEOUT);
         if (index == Win32Natives.WAIT_TIMEOUT) {
             // nothing happened.
 return;
         }
         if (index == Win32Natives.WAIT_FAILED) {
             // we ran into a problem
 int error = Win32Natives.GetLastError();
             if (error != Win32Natives.ERROR_INVALID_HANDLE && error != Win32Natives.ERROR_SUCCESS) {
                 addException(NLS.bind(Messages.WM_nativeErr, Integer.toString(error)));
                 refreshResult.monitorFailed(this, null);
             }
             return;
         }
         // a change occurred
 // WaitForMultipleObjects returns WAIT_OBJECT_0 + index
 index -= Win32Natives.WAIT_OBJECT_0;
         Handle handle = (Handle) fHandleValueToHandle.get(new Long (handleValues[index]));
         if (handle != null)
             handle.handleNotification();
     }
 }

